/*
 * Copyright (C) 2010 TopCoder Inc., All Rights Reserved.
 */
package com.hercules.xre.protocol.testing.handlers;

import com.hercules.xre.protocol.testing.EventHandler;
import com.hercules.xre.protocol.testing.EventHandlerException;
import com.hercules.xre.protocol.testing.runner.TestRunnerGUI;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * This class represents test case event handler. It provides features to delegate the implementation of handleXXX
 * methods to a set of event handlers registered to each method. It also writes execution reports to output file. It
 * has a set of setter methods to initialize the variables. This class extends BaseEventHandler that provides the
 * logger.
 *
 * <pre>
 * // create instance using default constructor
 * TestCaseEventHandler instance = new TestCaseEventHandler();
 * // if we need logging, we can set logger by name, directly
 * instance.setLoggerName(&quot;test&quot;);
 * instance.setLogger(LogManager.getLog(&quot;test&quot;));
 * // now we need to set file name, report will be written
 * instance.setReportFileName(&quot;test_files/out.bmp&quot;);
 * // also we can set if report will be appended or not
 * instance.setAppendToReportFile(true);
 * // and we should set event handlers for every event
 * // for now it will be empty to simplify the demo
 * Map&lt;EventType, List&lt;EventHandler&gt;&gt; eventHandlers = ...;
 * instance.setEventHandlers(eventHandlers);
 * // now we can call handleConnectionXXX methods
 * // this methods will run corresponding event handlers, their order is
 * // 1. one call of handleConnectionCreated
 * // 2. any number of calls of handleConnectionEvent
 * // 3. one call of handleConnectionDisconnected or handleConnectionShutdown
 * try {
 *     // 1.
 *     instance.handleConnectionCreated(&quot;event&quot;);
 *     // 2.
 *     instance.handleConnectionEvent(&quot;guid&quot;, &quot;event&quot;);
 *     instance.handleConnectionEvent(&quot;guid&quot;, &quot;event&quot;);
 *     instance.handleConnectionEvent(&quot;guid&quot;, &quot;event&quot;);
 *     // 3.
 *     instance.handleConnectionShutdown(&quot;guid&quot;, &quot;event&quot;);
 *     // or
 *     // instance.handleConnectionDisconnected(&quot;guid&quot;, new ArrayList&lt;String&gt;());
 *     // after this you can check report file, for information about failure or success
 * } catch (EventHandlerException e) {
 *     // handle the error
 *     e.printStackTrace();
 * }
 * </pre>
 * <p>
 * <b>Thread safety:</b> This class is effectively thread safe (see restriction below). It synchronizes on the methods
 * that would write to file. The following must be met for this class to be thread safe:
 * <li>getter/setter must be called before calling any other method
 * <li>the flow is either handleConnectionCreated -> handleConnectionEvent (can be multiple times) ->
 * handleConnectionDisconencted or handleConnectionShutdown.
 *
 * @author bramandia, 7realm
 * @version 1.0
 */
public class TestCaseEventHandler extends BaseEventHandler {
    /** Represents end of line separator. */
    private static final String EOL = System.getProperty("line.separator");
    /** Represents error message about I/O error. */
    private static final String IO_REPORT_ERROR = "I/O error occurred during reporting to file.";
    /** Represents message about general error in handleXXX methods. */
    private static final String GENERAL_HANDLER_ERROR = "Error occurred while handling connection event.";
    /**
     * Represents the map between the event type enumeration to the list of event handlers for that event. The key is
     * EventType enumeration. The value is a non-null (can be empty) list of EventHandler. The list must not have null
     * element. It is used in handleXXX methods. Initialized to null. Has getter and setter. Will not be null after
     * set.
     */
    private Map<EventType, List<EventHandler>> eventHandlers;
    /**
     * Represents the report filename where the report would be written. Initialized to null. Has getter & setter. Will
     * not be empty or null after set. Used in handleConnectionCreated
     */
    private String reportFileName;
    /**
     * Represents the flag whether this class would append to the existing report file or not. If not set, then the
     * existing file would be overwritten. Initialized to false. Has getter & setter. Used in handleConnectionCreated
     */
    private boolean appendToReportFile;
    /**
     * Represents the report file writer used to write to report file. Initialized in handleConnectionCreated. Will not
     * be null after initialization. Used in appendToReportFile method.
     */
    private BufferedWriter reportFileWriter;
    /**
     * Represents the list of errors accumulated in the execution of handleXXX methods. Initialized to empty array
     * list. Element added if there is exception. Used in handleConnectionShutdown and handleConnectionDisconnected.
     * Will not be changed.
     */
    private final List<String> errors = Collections.synchronizedList(new ArrayList<String>());

    /**
     * Represents the test runner gui.
     */
    private TestRunnerGUI runner;

    /**
     * Empty default constructor.
     */
    public TestCaseEventHandler() {
        // empty
    }

    /**
     * Gets the map between the event type enumeration to the list of event handlers for that event.
     *
     * @return the map between the event type enumeration to the list of event handlers for that event
     */
    public Map<EventType, List<EventHandler>> getEventHandlers() {
        String methodName = "getEventHandlers";
        Helper.logEnter(getLogger(), methodName);
        return Helper.logExit(getLogger(), methodName, eventHandlers);
    }

    /**
     * Sets the map between the event type enumeration to the list of event handlers for that event.
     *
     * @param eventHandlers the new value of the map between the event type enumeration to the list of event handlers
     *            for that event
     * @throws IllegalArgumentException if passed map is null, has null key or value, or value contain null items
     */
    public void setEventHandlers(Map<EventType, List<EventHandler>> eventHandlers) {
        String methodName = "setEventHandlers";

        try {
            // check argument
            Helper.logEnterAndCheck(getLogger(), methodName, "eventHandlers", eventHandlers);
            for (Entry<EventType, List<EventHandler>> entry : eventHandlers.entrySet()) {
                Helper.checkNull(entry.getKey(), "eventHandlers.Key");
                Helper.checkList(entry.getValue(), "eventHandlers.Value");
            }
            this.eventHandlers = eventHandlers;

            Helper.logExit(getLogger(), methodName);
        } catch (IllegalArgumentException e) {
            throw Helper.logIAE(getLogger(), e);
        }
    }

    /**
     * Gets the report filename where the report would be written.
     *
     * @return the report filename where the report would be written
     */
    public String getReportFileName() {
        String methodName = "getReportFileName";
        Helper.logEnter(getLogger(), methodName);
        return Helper.logExit(getLogger(), methodName, reportFileName);
    }

    /**
     * Sets the report filename where the report would be written.
     *
     * @param reportFileName the new value of the report filename where the report would be written
     * @throws IllegalArgumentException if argument is null/empty
     */
    public void setReportFileName(String reportFileName) {
        String methodName = "setReportFileName";

        try {
            // check argument
            Helper.logEnterAndCheck(getLogger(), methodName, "reportFileName", reportFileName);
            this.reportFileName = reportFileName;

            Helper.logExit(getLogger(), methodName);
        } catch (IllegalArgumentException e) {
            throw Helper.logIAE(getLogger(), e);
        }
    }

    /**
     * Gets the flag whether this class would append to the existing report file or not.
     *
     * @return the flag whether this class would append to the existing report file or not
     */
    @SuppressWarnings("boxing")
    public boolean getAppendToReportFile() {
        String methodName = "getAppendToReportFile";
        Helper.logEnter(getLogger(), methodName);
        return Helper.logExit(getLogger(), methodName, appendToReportFile);
    }

    /**
     * Sets the flag whether this class would append to the existing report file or not.
     *
     * @param appendToReportFile the new value of the flag whether this class would append to the existing report file
     *            or not
     */
    public void setAppendToReportFile(boolean appendToReportFile) {
        String methodName = "setAppendToReportFile";
        Helper.logEnter(getLogger(), methodName, "appendToReportFile", Boolean.valueOf(appendToReportFile));
        this.appendToReportFile = appendToReportFile;
        Helper.logExit(getLogger(), methodName);
    }

    /**
     * Handle the connection created.
     *
     * @param event the event to be handled, must not be null/empty
     * @throws IllegalArgumentException if any argument is invalid
     * @throws IllegalStateException if reportFileName or eventHandlers are not set
     * @throws ErrorInInnerHandlerException if the inner handler throw exception
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    public void handleConnectionCreated(String event) throws EventHandlerException {
        String methodName = "handleConnectionCreated";

        try {
            try {
                // check argument
                Helper.logEnterAndCheck(getLogger(), methodName, "event", event);

                // check state
                Helper.checkState(reportFileName != null && eventHandlers != null,
                        "Report file name and event handlers should be set before calling handleConnectionCreated.");

                // create report file
                reportFileWriter = new BufferedWriter(new FileWriter(reportFileName, appendToReportFile));
                errors.clear();
                Helper.log(getLogger(), "Report file name: '" + reportFileName + "'.");

                // execute handlers
                executeHandlers(EventType.CONNECTION_CREATED, event, null, null);

                runner.setConnected(true);
                Helper.logExit(getLogger(), methodName);
            } catch (IOException e) {
                throw new EventHandlerException(IO_REPORT_ERROR, e);
            }
        } catch (IllegalArgumentException e) {
            throw Helper.logIAE(getLogger(), e);
        } catch (IllegalStateException e) {
            throw Helper.logISE(getLogger(), e);
        } catch (EventHandlerException e) {
            throw Helper.logError(getLogger(), GENERAL_HANDLER_ERROR, e);
        }
    }

    /**
     * Handle the connection event.
     *
     * @param sessionGUID the unique session GUID for the application, must not be null/empty
     * @param event the event to be handled, must not be null/empty
     * @throws IllegalArgumentException if any argument is invalid
     * @throws IllegalStateException if reportFileWriter is not set
     * @throws ErrorInInnerHandlerException if the inner handler throw exception
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    public void handleConnectionEvent(String sessionGUID, String event) throws EventHandlerException {
        String methodName = "handleConnectionEvent";

        try {
            // check argument
            Helper.logEnterAndCheck(getLogger(), methodName, "sessionGUID", sessionGUID, "event", event);

            // check state
            Helper.checkState(reportFileWriter != null,
                    "Report file writer was not created in handleConnectionCreated.");

            try {
                runner.logMessage(event);
                // execute handlers
                executeHandlers(EventType.CONNECTION_EVENT, event, sessionGUID, null);

                Helper.logExit(getLogger(), methodName);
            } catch (IOException e) {
                throw new EventHandlerException(IO_REPORT_ERROR, e);
            }
        } catch (IllegalArgumentException e) {
            throw Helper.logIAE(getLogger(), e);
        } catch (IllegalStateException e) {
            throw Helper.logISE(getLogger(), e);
        } catch (EventHandlerException e) {
            throw Helper.logError(getLogger(), GENERAL_HANDLER_ERROR, e);
        }
    }

    /**
     * Handle the connection disconnected.
     *
     * @param sessionGUID the unique session GUID for the application, must not be null/empty
     * @param unsentCommands the list of unsentCommand strings to be handled, must not be null nor contain null/empty
     *            element
     * @throws IllegalArgumentException if any argument is invalid
     * @throws IllegalStateException if reportFileWriter is not set
     * @throws ErrorInInnerHandlerException if the inner handler throw exception
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    public void handleConnectionDisconnected(String sessionGUID, List<String> unsentCommands)
        throws EventHandlerException {
        handleCloseEvent("handleConnectionDisconnected", EventType.CONNECTION_DISCONNECTED, null, sessionGUID,
                unsentCommands, "sessionGUID", sessionGUID, "unsentCommands", unsentCommands);
    }

    /**
     * Handle the connection shutdown.
     *
     * @param sessionGUID the unique session GUID for the application, must not be null/empty
     * @param event the event to be handled, must not be null/empty
     * @throws IllegalArgumentException if any argument is invalid
     * @throws IllegalStateException if reportFileWriter is not set
     * @throws ErrorInInnerHandlerException if the inner handler throw exception
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    public void handleConnectionShutdown(String sessionGUID, String event) throws EventHandlerException {
        handleCloseEvent("handleConnectionShutdown", EventType.CONNECTION_SHUTDOWN, event, sessionGUID, null,
                "sessionGUID", sessionGUID, "event", event);
    }

    /**
     * Getter for the runner.
     *
     * @return the runner
     */
    public TestRunnerGUI getRunner() {
        return runner;
    }

    /**
     * Setter for the runner.
     *
     * @param runner the runner.
     */
    public void setRunner(TestRunnerGUI runner) {
        this.runner = runner;
    }

    /**
     * Handles close event.
     *
     * @param sessionGUID the session GUID
     * @param unsentCommands the unsent commands, can be empty
     * @param methodName the name of the method
     * @param eventType the type of event
     * @param event the event to be handled
     * @param params the calling method parameters
     * @throws IllegalArgumentException if any argument is wrong
     * @throws IllegalStateException if reportFileWriter is not set
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    private void handleCloseEvent(String methodName, EventType eventType, String event, String sessionGUID,
            List<String> unsentCommands, Object... params) throws EventHandlerException {
        try {
            // check argument
            Helper.logEnterAndCheck(getLogger(), methodName, params);

            // check state
            Helper.checkState(reportFileWriter != null,
                    "Report file writer was not created in handleConnectionCreated.");

            ErrorInInnerHandlerException storedException = null;
            runner.setConnected(false);
            try {
                try {
                    // execute handlers
                    executeHandlers(eventType, event, sessionGUID, unsentCommands);
                } catch (ErrorInInnerHandlerException e) {
                    storedException = e;
                }

                // log errors or success
                if (errors.size() == 0) {
                    appendToReportFile("The test case executed successfully.");
                } else {
                    for (String error : errors) {
                        appendToReportFile(error);
                    }
                    appendToReportFile("Execution of test case failed.");
                }

                // throw stored exception
                if (storedException != null) {
                    throw storedException;
                }

                Helper.logExit(getLogger(), methodName);
            } catch (IOException e) {
                throw new EventHandlerException(IO_REPORT_ERROR, e);
            } finally {
                closeReportFile();
            }
        } catch (IllegalArgumentException e) {
            throw Helper.logIAE(getLogger(), e);
        } catch (IllegalStateException e) {
            throw Helper.logISE(getLogger(), e);
        } catch (EventHandlerException e) {
            throw Helper.logError(getLogger(), GENERAL_HANDLER_ERROR, e);
        }
    }

    /**
     * Execute inner even handlers.
     *
     * @param eventType the current event type
     * @param event the event argument of calling method
     * @param sessionGUID the sessionGUID argument of calling method
     * @param unsentCommands the unsentCommands argument of calling method
     * @throws IOException if I/O error occurred during reporting to file
     * @throws ErrorInInnerHandlerException if error occurred in inner event handler
     */
    private void executeHandlers(EventType eventType, String event, String sessionGUID, List<String> unsentCommands)
        throws IOException, ErrorInInnerHandlerException {
        // get list of event handlers
        List<EventHandler> handlers = eventHandlers.get(eventType);
        if (handlers != null) {
            // call handlers one by one
            for (EventHandler eventHandler : handlers) {
                try {
                    Helper.log(getLogger(), "Executing handler: " + eventHandler + " for event " + eventType
                            + ", session GUID '" + sessionGUID + "'.");
                    switch (eventType) {
                    case CONNECTION_CREATED:
                        eventHandler.handleConnectionCreated(event);
                        break;
                    case CONNECTION_EVENT:
                        eventHandler.handleConnectionEvent(sessionGUID, event);
                        break;
                    case CONNECTION_DISCONNECTED:
                        eventHandler.handleConnectionDisconnected(sessionGUID, unsentCommands);
                        break;
                    // CONNECTION_SHUTDOWN
                    default:
                        eventHandler.handleConnectionShutdown(sessionGUID, event);
                        break;
                    }
                } catch (EventHandlerException e) {
                    reportError(eventType, event, e, eventHandler);
                    throw new ErrorInInnerHandlerException("Error occurred in inner handler: " + eventHandler
                            + " while handling event " + eventType + ".", e);
                }
            }
        }

        reportSuccess(eventType, event);
    }

    /**
     * Report the failed execution of test case phase to the report file.
     *
     * @param eventType the eventType
     * @param event the event
     * @param exception the exception causing the error
     * @param handler the handler causing the error
     */
    private void reportError(EventType eventType, String event, Exception exception, EventHandler handler) {
        StringWriter theError = new StringWriter();
        theError.write("Error occurred when executing event: ");
        theError.write(eventType.name());
        theError.write((event == null) ? ", " : (", event: " + event));
        theError.write(" on handler: ");
        theError.write(handler.getClass().getName());
        theError.write(EOL);
        theError.write("Exception class: ");
        theError.write(exception.getClass().getName());
        theError.write(", exception message: ");
        theError.write(exception.getMessage());
        theError.write(", exception stack trace: ");
        theError.write(EOL);

        // write stack trace
        PrintWriter stacktraceWriter = new PrintWriter(theError);
        exception.printStackTrace(stacktraceWriter);
        stacktraceWriter.flush();

        errors.add(theError.toString());
    }

    /**
     * Report success of test case phase execution to the report file.
     *
     * @param eventType the eventType
     * @param event the event
     * @throws IOException I/O error occurred during reporting to file
     */
    private void reportSuccess(EventType eventType, String event) throws IOException {
        appendToReportFile("Success in executing event " + eventType + ((event == null) ? "." : (", event: " + event)));
    }

    /**
     * Log the event.
     *
     * @param event the event.
     * @throws IOException if any error occurs while logging the event.
     */
    public void logEvent(String event) throws IOException {
        appendToReportFile("received event:" + event);
    }

    /**
     * Append the message to the report file.
     *
     * @param messages the messages to be added
     * @throws IOException I/O error occurred during appending to file
     */
    private synchronized void appendToReportFile(String... messages) throws IOException {
        for (String message : messages) {
            reportFileWriter.write(runner.logMessage(message));
        }
        reportFileWriter.newLine();
    }

    /**
     * Close the report file.
     */
    public synchronized void closeReportFile() {
        try {
            if (reportFileWriter != null) {
                reportFileWriter.close();
            }
        } catch (IOException e) {
            // ignore
        } finally {
            reportFileWriter = null;
        }
    }
}
